package com.itzg.fileutil;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;

import org.apache.commons.math.random.RandomDataImpl;

public class DirTreeFiller {

	private static final int LAST_CHAR = 0x7d;
	private static final int FIRST_CHAR = 0x20;
	private static final String SUFFIX = ".txt";
	private static final char LF = 0xA;
	private final File baseDir;
	private final Scenario scenario;
	
	private long elementCount = 0;

	private long totalFileSize = 0;

	private RandomDataImpl rand = new RandomDataImpl();
	
	private FillerProgressListener progressListener;
	private long totalFileSizeLimit;
	private long totalElementLimit;

	public DirTreeFiller(File baseDir, Scenario scenario) {
		this.baseDir = baseDir;
		this.scenario = scenario;
		if (scenario.getSeed() != null) {
			rand.reSeed(scenario.getSeed());
		}
	}

	public void fill(boolean deleteFirst) throws IOException {
		if (deleteFirst) {
			deleteDir(baseDir);
		}
		fill();
	}
	
	public void fill() throws IOException {
		totalFileSizeLimit = scenario.getFilesize().total.limit*scenario.getFilesize().total.scale;
		totalElementLimit = scenario.getElements().limit;
		baseDir.mkdir();
		while (fillSubDir(baseDir, nextDepth(), 0)) {
		}
		// tell them one last time, so they'll see a 100%
		tellListener();
	}
	
	public static void deleteDir(File dir) {
		if (dir.isDirectory()) {
			File[] files = dir.listFiles();
			for (File file : files) {
				if (file.isFile()) {
					file.delete();
				}
				else {
					deleteDir(file);
				}
			}
		}
		dir.delete();
	}

	/**
	 * 
	 * @param parentDir
	 * @param limitDepth
	 * @param yourDepth
	 * @return true if good to proceed or false if hit a limit
	 * @throws IOException
	 */
	protected boolean fillSubDir(final File parentDir, final int limitDepth, final int yourDepth) throws IOException {
		File dir = new File(parentDir, nextFilename());
		if (!dir.mkdir()) {
			throw new IOException("Failed to create "+dir);
		}
		++elementCount;
		if (elementCount >= totalElementLimit) {
			return false;
		}
		int fileCount = nextFileCount();
		for (int i = 0; i < fileCount; ++i) {
			fillFile(dir);
			++elementCount;
			if (elementCount >= totalElementLimit || 
					totalFileSize >= totalFileSizeLimit) {
				return false;
			}
		}
		if (yourDepth < limitDepth) {
			int dirCount = nextDirCount();
			for (int d = 0; d < dirCount && elementCount < scenario.getElements().limit; ++d) {
				int nextLimitDepth = d == 0 ? limitDepth : nextDepth();
				if (!fillSubDir(dir, nextLimitDepth, yourDepth+1)) {
					return false;
				}
			}
		}
		tellListener();
		return true;
	}

	private void tellListener() {
		if (progressListener != null) {
			float filledByElement = (float)elementCount / scenario.getElements().limit;
			float filledBySize = (float)totalFileSize / (scenario.getFilesize().total.limit*scenario.getFilesize().total.scale);
			progressListener.updatedProgress(filledByElement, filledBySize);
		}
	}

	private int nextDirCount() {
		return (int) enforceLimit(nextDistributedLong(scenario.getDirsPerDir()), 
				elementCount, scenario.getElements().limit);
	}

	private long nextDistributedLong(GaussianSpec spec) {
		long result = (long) rand.nextGaussian(spec.mu, spec.sigma);
		if (result < spec.min) {
			result = spec.min;
		}
		return result;
	}

	protected void fillFile(File dir) throws IOException {
		File file = new File(dir, nextFilename()+SUFFIX);
		final long thisTotalFilesize = nextFilesize();
		if (thisTotalFilesize == 0) return;
		
		FileOutputStream fos = new FileOutputStream(file);
		OutputStreamWriter writer = new OutputStreamWriter(fos, Charset.forName("UTF-8"));
		try {
			char c = nextStartingChar();
			int maxLineLength = scenario.getLine().length.fixed;
			char[] line = new char[maxLineLength];
			long filesize = 0;
			while (filesize < thisTotalFilesize) {
				int lineLength = (int) enforceLimit(maxLineLength, filesize, thisTotalFilesize);
				for (int lPos = 0; lPos < lineLength-1; ++lPos) {
					line[lPos] = c++;
					if (c > LAST_CHAR) {
						c = FIRST_CHAR;
					}
				}
				line[lineLength-1] = LF;
				writer.write(line, 0, lineLength);
				filesize += lineLength;
			}
		} finally {
			writer.close();
			fos.close();
			totalFileSize += thisTotalFilesize;
		}
	}

	private char nextStartingChar() {
		return (char) rand.nextInt(FIRST_CHAR, LAST_CHAR);
	}

	private long enforceLimit(long attempt, long priorSum, final long limit) {
		long result = attempt;
		if (priorSum+result > limit) {
			result = limit - priorSum;
		}
		return result;
	}

	private long nextFilesize() {
		long size = nextDistributedLong(scenario.getFilesize().eachFile);
		long filesizeTotalLimitInBytes = scenario.getFilesize().total.limit*scenario.getFilesize().total.scale;
		if (size+totalFileSize > filesizeTotalLimitInBytes) {
			size = filesizeTotalLimitInBytes - totalFileSize;
		}
		return size;
	}

	private int nextFileCount() {
		return (int) enforceLimit(nextDistributedLong(scenario.getFilesPerDir()),
				elementCount, scenario.getElements().limit);
	}

	private String nextFilename() {
		return rand.nextHexString(8);
	}

	private int nextDepth() {
		return (int) nextDistributedLong(scenario.getDepth());
	}

	public FillerProgressListener getProgressListener() {
		return progressListener;
	}

	public void setProgressListener(FillerProgressListener progressListener) {
		this.progressListener = progressListener;
	}

}
